/*
 Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights
 reserved.
 
 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License, version 2.0,
 as published by the Free Software Foundation.

 This program is also distributed with certain software (including
 but not limited to OpenSSL) that is licensed under separate terms,
 as designated in a particular file or component or in included license
 documentation.  The authors of MySQL hereby grant you an additional
 permission to link the program and your derivative works with the
 separately licensed software that they have included with MySQL.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License, version 2.0, for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 02110-1301  USA
*/

"use strict";

var stats = {
	"constructor_calls"      : 0,
	"created"                : {},
	"default_mappings"       : 0,
	"explicit_mappings"      : 0,
	"return_null"            : 0,
	"result_objects_created" : 0,
	"DBIndexHandler_created" : 0
};

var assert          = require("assert"),
    TableMapping    = require(mynode.api.TableMapping).TableMapping,
    FieldMapping    = require(mynode.api.TableMapping).FieldMapping,
    stats_module    = require(mynode.api.stats),
    util            = require("util"),
    udebug          = unified_debug.getLogger("DBTableHandler.js");

// forward declaration of DBIndexHandler to avoid lint issue
var DBIndexHandler;

stats_module.register(stats,"spi","DBTableHandler");

/* A DBTableHandler (DBT) combines dictionary metadata with user mappings.  
   It manages setting and getting of columns based on the fields of a 
   user's domain object.  It can also choose an index access path by 
   comapring user-supplied key fields of a domain object with a table's indexes.
   
   These are the structural parts of a DBT: 
     * mapping, an API TableMapping, either created explicitly or by default.
     * A TableMetadata object, obtained from the data dictionary.
     * An internal set of maps between Fields and Columns
     
    The mapping and TableMetadata are supplied as arguments to the 
    constructor, which creates the maps.
    
    Some terms: 
      column number: column order in table as supplied by DataDictionary
      field number: an arbitrary ordering of only the mapped fields 
*/


/* getColumnByName() is a utility function used in the building of maps.
*/
function getColumnByName(dbTable, colName) {
  udebug.log_detail("getColumnByName", colName);
  var i, col;
  
  for(i = 0 ; i < dbTable.columns.length ; i++) {
    col = dbTable.columns[i];
    if(col.name === colName) {
      return col;
    }
  }
  udebug.log("getColumnByName", colName, "NOT FOUND.");
  return null;
}


/* DBTableHandler() constructor
   IMMEDIATE

   Create a DBTableHandler for a table and a mapping.

   The TableMetadata may not be null.

   If the TableMapping is null, default mapping behavior will be used.
   Default mapping behavior is to:
     select all columns when reading
     use default domainTypeConverters for all data types
     perform no remapping between field names and column names
*/
function DBTableHandler(dbtable, tablemapping, ctor) {
  assert(arguments.length === 3);
  var i,               // an iterator
      f,               // a FieldMapping
      c,               // a ColumnMetadata
      n,               // a field or column number
      index,           // a DBIndex
      stubFields,      // fields created through default mapping
      foreignKey,      // foreign key object from dbTable
      nMappedFields;

  stats.constructor_calls++;

  if(! ( dbtable && dbtable.columns)) {
    stats.return_null++;
    return null;
  }

	if(typeof stats.created[dbtable.name] === 'undefined') {
		stats.created[dbtable.name] = 1;
	} else { 
		stats.created[dbtable.name]++;
	}
  
  this.dbTable = dbtable;

  if(ctor) {
    this.newObjectConstructor = ctor;
  }

  if(tablemapping) {     
    stats.explicit_mappings++;
    this.mapping = tablemapping;
  }
  else {                                          // Create a default mapping
    stats.default_mappings++;
    this.mapping          = new TableMapping(this.dbTable.name);
    this.mapping.database = this.dbTable.database;
  }
  
  /* Default properties */
  this.resolvedMapping        = null;
  this.ValueObject            = null;
  this.errorMessages          = '\n';
  this.isValid                = true;
  this.autoIncFieldName       = null;
  this.autoIncColumnNumber    = null;
  this.numberOfLobColumns     = 0;
  this.numberOfNotPersistentFields = 0;
   
  /* New Arrays */
  this.columnNumberToFieldMap = [];  
  this.fieldNumberToColumnMap = [];
  this.fieldNumberToFieldMap  = [];
  this.fieldNameToFieldMap    = {};
  this.foreignKeyMap          = {};
  this.dbIndexHandlers        = [];
  this.relationshipFields     = [];

  /* Build the first draft of the columnNumberToFieldMap, using only the
     explicitly mapped fields. */
  if (typeof(this.mapping.fields) === 'undefined') {
    this.mapping.fields = [];
  }
  for(i = 0 ; i < this.mapping.fields.length ; i++) {
    f = this.mapping.fields[i];
    udebug.log_detail('DBTableHandler<ctor> field:', f, 'persistent', f.persistent, 'relationship', f.relationship);
    if(f && f.persistent) {
      if (!f.relationship) {
        c = getColumnByName(this.dbTable, f.columnName);
        if(c) {
          n = c.columnNumber;
          this.columnNumberToFieldMap[n] = f;
          f.columnNumber = n;
          f.defaultValue = c.defaultValue;
          f.databaseTypeConverter = c.databaseTypeConverter;
          // use converter or default domain type converter
          if (f.converter) {
            udebug.log_detail('domain type converter for ', f.columnName, ' is user-specified ', f.converter);
            f.domainTypeConverter = f.converter;
          } else {
            udebug.log_detail('domain type converter for ', f.columnName, ' is system-specified ', c.domainTypeConverter);
            f.domainTypeConverter = c.domainTypeConverter;
          }
        } else {
          this.appendErrorMessage(
              'for table ' + dbtable.name + ', field ' + f.fieldName + ': column ' + f.columnName + ' does not exist.');
        }
      } else {
        // relationship field
        this.relationshipFields.push(f);
      }
    } else {
      // increment not-persistent field count
      ++this.numberOfNotPersistentFields;
    }
  }

  /* Now build the implicitly mapped fields and add them to the map */
  stubFields = [];
  if(this.mapping.mapAllColumns) {
    for(i = 0 ; i < this.dbTable.columns.length ; i++) {
      if(! this.columnNumberToFieldMap[i]) {
        c = this.dbTable.columns[i];
        f = new FieldMapping(c.name);
        stubFields.push(f);
        this.columnNumberToFieldMap[i] = f;
        f.columnNumber = i;
        f.defaultValue = c.defaultValue;
        f.databaseTypeConverter = c.databaseTypeConverter;
        // use converter or default domain type converter
        if (f.converter) {
          udebug.log_detail('domain type converter for ', f.columnName, ' is user-specified ', f.converter);
          f.domainTypeConverter = f.converter;
        } else {
          udebug.log_detail('domain type converter for ', f.columnName, ' is system-specified ', c.domainTypeConverter);
          f.domainTypeConverter = c.domainTypeConverter;
        }
      }
    }
  }

  /* Total number of mapped fields */
  nMappedFields = this.mapping.fields.length + stubFields.length - this.numberOfNotPersistentFields;
         
  /* Create the resolved mapping to be returned by getMapping() */
  this.resolvedMapping = {};
  this.resolvedMapping.database = this.dbTable.database;
  this.resolvedMapping.table = this.dbTable.name;
  this.resolvedMapping.fields = [];

  /* Build fieldNumberToColumnMap, establishing field order.
     Detect the autoincrement column.
     Also build the remaining fieldNameToFieldMap and fieldNumberToFieldMap. */
  for(i = 0 ; i < this.dbTable.columns.length ; i++) {
    c = this.dbTable.columns[i];
    f = this.columnNumberToFieldMap[i];
    if(c.isAutoincrement) { 
      this.autoIncColumnNumber = i;
      this.autoIncFieldName = f.fieldName;
    }
    if(c.isLob) {
      this.numberOfLobColumns++;
    }    
    this.resolvedMapping.fields[i] = {};
    if(f) {
      f.fieldNumber = i;
      this.fieldNumberToColumnMap.push(c);
      this.fieldNumberToFieldMap.push(f);
      this.fieldNameToFieldMap[f.fieldName] = f;
      this.resolvedMapping.fields[i].columnName = f.columnName;
      this.resolvedMapping.fields[i].fieldName = f.fieldName;
      this.resolvedMapping.fields[i].persistent = true;
    }
  }
  var map = this.fieldNameToFieldMap;
  // add the relationship fields that are not mapped to columns
  this.relationshipFields.forEach(function(relationship) {
    map[relationship.fieldName] = relationship;
  });
  
  if (nMappedFields !== this.fieldNumberToColumnMap.length + this.relationshipFields.length) {
    this.appendErrorMessage('Mismatch between number of mapped fields and columns for ' + ctor.prototype.constructor.name);
  }

  // build dbIndexHandlers; one for each dbIndex, starting with primary key index 0
  for (i = 0; i < this.dbTable.indexes.length; ++i) {
    // a little fix-up for primary key unique index:
    index = this.dbTable.indexes[i];
    udebug.log_detail('DbTableHandler<ctor> creating DBIndexHandler for', index);
    if (typeof(index.name) === 'undefined') {
      index.name = 'PRIMARY';
    }
    this.dbIndexHandlers.push(new DBIndexHandler(this, index));
  }
  // build foreign key map
  for (i = 0; i < this.dbTable.foreignKeys.length; ++i) {
    foreignKey = this.dbTable.foreignKeys[i];
    this.foreignKeyMap[foreignKey.name] = foreignKey;
  }

  if (!this.isValid) {
    this.err = new Error(this.errorMessages);
  }
  
  if (ctor) {
    // cache this in ctor.prototype.mynode.dbTableHandler
    if (!ctor.prototype.mynode) {
      ctor.prototype.mynode = {};
    }
    if (!ctor.prototype.mynode.dbTableHandler) {
      ctor.prototype.mynode.dbTableHandler = this;
    }
  }
  udebug.log("new completed");
  udebug.log_detail("DBTableHandler<ctor>:\n", this);
}


/** Append an error message and mark this DBTableHandler as invalid.
 */
DBTableHandler.prototype.appendErrorMessage = function(msg) {
  this.errorMessages += '\n' + msg;
  this.isValid = false;
};


/* DBTableHandler.newResultObject
   IMMEDIATE
   
   Create a new object using the constructor function (if set).
*/
DBTableHandler.prototype.newResultObject = function(values, adapter) {
  udebug.log("newResultObject");
  stats.result_objects_created++;
  var newDomainObj;
  
  if(this.newObjectConstructor && this.newObjectConstructor.prototype) {
    newDomainObj = Object.create(this.newObjectConstructor.prototype);
  }
  else {
    newDomainObj = {};
  }
  
  if(this.newObjectConstructor) {
    udebug.log("newResultObject calling user constructor");
    this.newObjectConstructor.call(newDomainObj);
  }

  if (typeof(values) === 'object') {
    // copy values into the new domain object
    this.setFields(newDomainObj, values, adapter);
  }
  udebug.log("newResultObject done", newDomainObj);
  return newDomainObj;
};


/* DBTableHandler.newResultObjectFromRow
 * IMMEDIATE

 * Create a new object using the constructor function (if set).
 * Values for the object's fields come from the row; first the key fields
 * and then the non-key fields. The row contains items named '0', '1', etc.
 * The value for the first key field is in row[offset]. Values obtained
 * from the row are first processed by the db converter and type converter
 * if present.
 */
DBTableHandler.prototype.newResultObjectFromRow = function(row, adapter,
    offset, keyFields, nonKeyFields) {
  var fieldIndex;
  var rowValue;
  var field;
  var newDomainObj;

  udebug.log("newResultObjectFromRow");
  stats.result_objects_created++;

  if(this.newObjectConstructor && this.newObjectConstructor.prototype) {
    newDomainObj = Object.create(this.newObjectConstructor.prototype);
  } else {
    newDomainObj = {};
  }

  if(this.newObjectConstructor) {
    udebug.log("newResultObject calling user constructor");
    this.newObjectConstructor.call(newDomainObj);
  }
  // set key field values from row using type converters

  for (fieldIndex = 0; fieldIndex < keyFields.length; ++fieldIndex) {
    rowValue = row[offset + fieldIndex];
    field = keyFields[fieldIndex];
    this.set(newDomainObj, field.fieldNumber, rowValue, adapter);
  }
  
  // set non-key field values from row using type converters
  offset += keyFields.length;
  for (fieldIndex = 0; fieldIndex < nonKeyFields.length; ++fieldIndex) {
    rowValue = row[offset + fieldIndex];
    field = nonKeyFields[fieldIndex];
    this.set(newDomainObj, field.fieldNumber, rowValue, adapter);
  }
  
  udebug.log("newResultObjectFromRow done", newDomainObj.constructor.name, newDomainObj);
  return newDomainObj;
};

/** applyMappingToResult(object)
 * IMMEDIATE
 * Apply the table mapping to the result object. The result object
 * has properties corresponding to field names whose values came
 * from the database. If a domain object is needed, a new domain
 * object is created and values are copied from the result object.
 * The result (either the original result object or a new domain
 * object) is returned.
 * @param obj the object to which to apply mapping
 * @return the object to return to the user
 */
DBTableHandler.prototype.applyMappingToResult = function(obj, adapter) {
  if (this.newObjectConstructor) {
    // create the domain object from the result
    obj = this.newResultObject(obj, adapter);
  } else {
    this.applyFieldConverters(obj, adapter);
  }
  return obj;
};


/** applyFieldConverters(object) 
 *  IMMEDIATE
 *  Apply the field converters to an existing object
 */ 
DBTableHandler.prototype.applyFieldConverters = function(obj, adapter) {
  var i, f, value, convertedValue;

  for (i = 0; i < this.fieldNumberToFieldMap.length; i++) {
    f = this.fieldNumberToFieldMap[i];
    var databaseTypeConverter = f.databaseTypeConverter && f.databaseTypeConverter[adapter];
    if (databaseTypeConverter) {
      value = obj[f.fieldName];
      convertedValue = databaseTypeConverter.fromDB(value);
      obj[f.fieldName] = convertedValue;
    }
    if(f.domainTypeConverter) {
      value = obj[f.fieldName];
      convertedValue = f.domainTypeConverter.fromDB(value, obj, f);
      obj[f.fieldName] = convertedValue;
    }
  }
};


/* setAutoincrement(object, autoincrementValue) 
 * IMMEDIATE
 * Store autoincrement value into object
 */
DBTableHandler.prototype.setAutoincrement = function(object, autoincrementValue) {
  if(typeof this.autoIncColumnNumber === 'number') {
    object[this.autoIncFieldName] = autoincrementValue;
    udebug.log("setAutoincrement", this.autoIncFieldName, ":=", autoincrementValue);
  }
};


/* getMappedFieldCount()
   IMMEDIATE   
   Returns the number of fields mapped to columns in the table 
*/
DBTableHandler.prototype.getMappedFieldCount = function() {
  udebug.log_detail("getMappedFieldCount");
  return this.fieldNumberToColumnMap.length;
};


/* allColumnsMapped()
   IMMEDIATE   
   Boolean: returns True if all columns are mapped
*/
DBTableHandler.prototype.allColumnsMapped = function() {
  return (this.dbTable.columns.length === this.fieldNumberToColumnMap.length);
};

/** allFieldsIncluded(values)
 *  IMMEDIATE
 *  returns array of indexes of fields included in values
 */
DBTableHandler.prototype.allFieldsIncluded = function(values) {
  // return a list of fields indexes that are found
  // the caller can easily construct the appropriate database statement
  var i, f, result = [];
  for (i = 0; i < this.fieldNumberToFieldMap.length; ++i) {
    f = this.fieldNumberToFieldMap[i];
    if (typeof(values[i]) !== 'undefined') {
      result.push(i);
    }
  }
  return result;
};

/* getColumnMetadata() 
   IMMEDIATE 
   
   Returns an array containing ColumnMetadata objects in field order
*/   
DBTableHandler.prototype.getColumnMetadata = function() {
  return this.fieldNumberToColumnMap;
};


/* IndexMetadata chooseIndex(dbTableHandler, keys) 
 Returns the index number to use as an access path.
 From API Context.find():
   * The parameter "keys" may be of any type. Keys must uniquely identify
   * a single row in the database. If keys is a simple type
   * (number or string), then the parameter type must be the 
   * same type as or compatible with the primary key type of the mapped object.
   * Otherwise, properties are taken
   * from the parameter and matched against property names in the
   * mapping.
*/
function chooseIndex(self, keys, uniqueOnly) {
  udebug.log("chooseIndex");
  var idxs = self.dbTable.indexes;
  var keyFieldNames, firstIdxFieldName;
  var i, j, f, n, index, nmatches, x;
  
  udebug.log_detail("chooseIndex for:", JSON.stringify(keys));
  
  if(typeof keys === 'number' || typeof keys === 'string') {
    if(idxs[0].columnNumbers.length === 1) {
      return 0;
    }
  }
  else {
    /* Keys is an object */ 
     keyFieldNames = [];
     for (x in keys) {
       // only include properties of the keys itself that are defined and not null
       if (keys.hasOwnProperty(x) && keys[x]) {
         keyFieldNames.push(x);
       }
     }

    /* First look for a unique index.  All columns must match. */
    for(i = 0 ; i < idxs.length ; i++) {
      index = idxs[i];
      if(index.isUnique) {
        udebug.log_detail("Considering:", (index.name || "primary key ") + " for " + JSON.stringify(keys));
        // Each key field resolves to a column, which must be in the index
        nmatches = 0;
        for(j = 0 ; j < index.columnNumbers.length ; j++) {
          n = index.columnNumbers[j];
          f = self.columnNumberToFieldMap[n]; 
          udebug.log_detail("index part", j, "is column", n, ":", f.fieldName);
          if(typeof keys[f.fieldName] !== 'undefined') {
            nmatches++;
            udebug.log_detail("match! ", nmatches);
          }
        }
        if(nmatches === index.columnNumbers.length) {
          udebug.log("chooseIndex picked unique index", i);
          return i; // all columns are found in the key object
        }
      }    
    }

    // if unique only, return failure
    if (uniqueOnly) {
      udebug.log("chooseIndex for unique index FAILED");
      return -1;
    }

    /* Then look for an ordered index.  A prefix match is OK. */
    /* Return the first suitable index we find (which might not be the best) */
    /* TODO: A better algorithm might be to return the one with the longest train of matches */
    for(i = 0 ; i < idxs.length ; i++) {
      index = idxs[i];
      if(index.isOrdered) {
        // f is the field corresponding to the first column in the index
        f = self.columnNumberToFieldMap[index.columnNumbers[0]];
        if(keyFieldNames.indexOf(f.fieldName) >= 0) {
         udebug.log("chooseIndex picked ordered index", i);
         return i; // this is an ordered index scan
        }
      }
    }
  }

  udebug.log("chooseIndex FAILED");
  return -1; // didn't find a suitable index
}


/** Return the property of obj corresponding to fieldNumber.
 * If a domain type converter and/or database type converter is defined, convert the value here.
 * If a fieldValueDefinedListener is passed, notify it via setDefined or setUndefined for each column.
 * Call setDefined if a column value is defined in the object and setUndefined if not.
 */
DBTableHandler.prototype.get = function(obj, fieldNumber, adapter, fieldValueDefinedListener) { 
  udebug.log_detail("get", fieldNumber);
  if (typeof(obj) === 'string' || typeof(obj) === 'number') {
    if (fieldValueDefinedListener) {
      fieldValueDefinedListener.setDefined(fieldNumber);
    }
    return obj;
  }
  var f = this.fieldNumberToFieldMap[fieldNumber];
  var result;
  if (!f) {
    throw new Error('FatalInternalError: field number does not exist: ' + fieldNumber);
  }
  if(f.domainTypeConverter) {
    result = f.domainTypeConverter.toDB(obj[f.fieldName], obj, f);
  }
  else {
    result = obj[f.fieldName];
  }
  var databaseTypeConverter = f.databaseTypeConverter && f.databaseTypeConverter[adapter];
  if (databaseTypeConverter && result !== undefined) {
    result = databaseTypeConverter.toDB(result);
  }
  if (fieldValueDefinedListener) {
    if (typeof(result) === 'undefined') {
      fieldValueDefinedListener.setUndefined(fieldNumber);
    } else {
      if (this.fieldNumberToColumnMap[fieldNumber].isBinary && result.constructor && result.constructor.name !== 'Buffer') {
        var err = new Error('Binary field with non-Buffer data for field ' + f.fieldName);
        err.sqlstate = '22000';
        fieldValueDefinedListener.err = err;
      }
      fieldValueDefinedListener.setDefined(fieldNumber);
    }
  }
  return result;
};


/** Return the property of obj corresponding to fieldNumber.
*/
DBTableHandler.prototype.getFieldsSimple = function(obj, fieldNumber) {
  var f;
  f = this.fieldNumberToFieldMap[fieldNumber];
  if(f.domainTypeConverter) {
    return f.domainTypeConverter.toDB(obj[f.fieldName], obj, f);
  }
  return obj[f.fieldName];
};
  
  
/* Return an array of values in field order */
DBTableHandler.prototype.getFields = function(obj) {
  var i, n, fields;
  fields = [];
  n = this.getMappedFieldCount();
  switch(typeof obj) {
    case 'number':
    case 'string':
      fields.push(obj);
      break;
    default: 
      for(i = 0 ; i < n ; i++) { fields.push(this.getFieldsSimple(obj, i)); }
  }
  return fields;
};


/* Return an array of values in field order */
DBTableHandler.prototype.getFieldsWithListener = function(obj, adapter, fieldValueDefinedListener) {
  var i, fields = [];
  for( i = 0 ; i < this.getMappedFieldCount() ; i ++) {
    fields[i] = this.get(obj, i, adapter, fieldValueDefinedListener);
  }
  return fields;
};


/* Set field to value */
DBTableHandler.prototype.set = function(obj, fieldNumber, value, adapter) {
  udebug.log_detail("set", fieldNumber);
  var f = this.fieldNumberToFieldMap[fieldNumber];
  var userValue = value;
  var databaseTypeConverter;
  if(f) {
    databaseTypeConverter = f.databaseTypeConverter && f.databaseTypeConverter[adapter];
    if (databaseTypeConverter) {
      userValue = databaseTypeConverter.fromDB(value);
    }
    if(f.domainTypeConverter) {
      userValue = f.domainTypeConverter.fromDB(userValue, obj, f);
    }
    obj[f.fieldName] = userValue;
    return true; 
  }
  return false;
};


/* Set all member values of object from a value object, which
 * has properties corresponding to field names. 
 * User-defined column conversion is handled in the set method.
*/
DBTableHandler.prototype.setFields = function(obj, values, adapter) {
  var i, f, value, columnName, fieldName;
  for (i = 0; i < this.fieldNumberToFieldMap.length; ++i) {
    f = this.fieldNumberToFieldMap[i];
    columnName = f.columnName;
    fieldName = f.fieldName;
    value = values[fieldName];
    if (value !== undefined) {
      this.set(obj, i, value, adapter);
    }
  }
};


/* DBIndexHandler constructor and prototype */
DBIndexHandler = function (parent, dbIndex) {
  udebug.log("DBIndexHandler constructor");
  stats.DBIndexHandler_created++;
  var i, colNo;

  this.tableHandler = parent;
  this.dbIndex = dbIndex;
  this.fieldNumberToColumnMap = [];
  this.fieldNumberToFieldMap  = [];
  
  for(i = 0 ; i < dbIndex.columnNumbers.length ; i++) {
    colNo = dbIndex.columnNumbers[i];
    this.fieldNumberToFieldMap[i]  = parent.columnNumberToFieldMap[colNo];
    this.fieldNumberToColumnMap[i] = parent.dbTable.columns[colNo];
  }
  
  if(i === 1) {    // One-column index
    this.singleColumn = this.fieldNumberToColumnMap[0];
  } else {
    this.singleColumn = null;
  }
};

/* DBIndexHandler inherits some methods from DBTableHandler */
DBIndexHandler.prototype = {
  getMappedFieldCount    : DBTableHandler.prototype.getMappedFieldCount,   
  get                    : DBTableHandler.prototype.get,   
  getFieldsSimple        : DBTableHandler.prototype.getFieldsSimple,
  getFields              : DBTableHandler.prototype.getFields,
  getColumnMetadata      : DBTableHandler.prototype.getColumnMetadata
};


/* DBIndexHandler getIndexHandler(Object keys)
   IMMEDIATE

   Given an object containing keys as defined in API Context.find(),
   choose an index to use as an access path for the operation,
   and return a DBIndexHandler for that index.
*/
DBTableHandler.prototype.getIndexHandler = function(keys, uniqueOnly) {
  udebug.log("getIndexHandler");
  var idx = chooseIndex(this, keys, uniqueOnly);
  var handler = null;
  if (idx !== -1) {
    handler = this.dbIndexHandlers[idx];
  }
  return handler;
};

DBTableHandler.prototype.getForeignKey = function(foreignKeyName) {
  return this.foreignKeyMap[foreignKeyName];
};


exports.DBTableHandler = DBTableHandler;
